using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Net.Sockets;
using System.Threading;
using Trace=InfiniTec.Diagnostics.Trace;

namespace InfiniTec.Threading
{
	[Localizable(false)]
	public class AsyncOperation
    {
        private static readonly ResourceReservationCollection _ResourceReservations = new ResourceReservationCollection();
        [ThreadStatic] private static AsyncOperation _Current;

        private static Trace _Trace = new Trace("Infinitec.Threading");
		private readonly object _AdvanceLock = new object();

        private readonly bool _AllowMultipleInvocations;
        private readonly AsyncOperation _BaseOperation;

        private readonly List<OperationInfo> _ChildOperations = new List<OperationInfo>();
        private readonly object _ChildOperationsLock = new object();
        private readonly Guid _Id = Guid.NewGuid();
        private readonly string _Name;
        private readonly IEnumerable<OperationAction> _Operation;
        private readonly IEnumerator<OperationAction> _OperationsEnumerator;
        private readonly SynchronizationContext _SynchronizationContext = SynchronizationContext.Current ?? new SynchronizationContext();
		private readonly object _TimeoutLock = new object();
        private readonly object _UserState;
        private Exception _Error;
        private int _MaximumParallelChildOperations = int.MaxValue;
        private int _EventCallbacksOccurred;
		

        private volatile ManualResetEvent _OperationCompletedSignal;
        private OperationStatus _OperationStatus;
        private AsyncOperation _Parent;
        private IResourceConstraint _ResourceConstraint;
        private bool _RunningSynchronously;
        private DateTime _StartTime;
        private TimeSpan? _Timeout;
        private DispatcherCookie _TimeoutHandle;
        private AsyncOperationTracker _Tracker;
        private DispatcherCookie _WaitOperationCookie;
    	private int _Unwinded;

        /// <summary>
        /// Initializes a new instance of the <see cref="AsyncOperation"/> class.
        /// </summary>
        /// <param name="name">The name of the operation</param>
        /// <param name="operation">The operation to execute.</param>
        /// <param name="tracker">The tracker in which this operation is enlisted.</param>
        /// <param name="allowMultipleInvocations">if set to <c>true</c> the operation can be called multiple times.</param>
        /// <param name="userState">A user defined stated which can be used to identify this instance.</param>
        public AsyncOperation(string name, IEnumerable<OperationAction> operation, AsyncOperationTracker tracker, bool allowMultipleInvocations,
                              object userState) : this(operation, userState)
        {
            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");

            _Name = name;
            _Tracker = tracker;
            _AllowMultipleInvocations = allowMultipleInvocations;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AsyncOperation"/> 
        /// </summary>
        /// <param name="operation">The operation to execute</param>
        /// <param name="userState">A user defined stated which can be used to identify this instance.</param>
        public AsyncOperation(IEnumerable<OperationAction> operation, object userState)
        {
            if (operation == null) throw new ArgumentNullException("operation");

            _UserState = userState;
            _Operation = operation;
            _OperationsEnumerator = _Operation.GetEnumerator();
        }

		public void CompleteAsyncOperation(IAsyncResult ar)
		{
			if (ar.CompletedSynchronously) return;

			Advance();
		}

    	/// <summary>
        /// Initializes a new instance of the <see cref="AsyncOperation"/> class and provides access to the base operation
        /// provided by <see cref="baseOperation"/> via the <see cref="ExecuteBaseOperation"/> action.
        /// </summary>
        /// <param name="operation">The operation to execute.</param>
        /// <param name="baseOperation">The base operation to execute when a <see cref="ExecuteBaseOperation"/> is returned by the <see cref="operation"/>.</param>
        /// <param name="userState">A user defined stated which can be used to identify this instance.</param>
        public AsyncOperation(IEnumerable<OperationAction> operation, AsyncOperation baseOperation, object userState)
            : this(operation, userState)
        {
            if (baseOperation == null) throw new ArgumentNullException("baseOperation");

            _BaseOperation = baseOperation;
            _Tracker = _BaseOperation._Tracker;
            _Name = _BaseOperation.Name;
            _AllowMultipleInvocations = _BaseOperation._AllowMultipleInvocations;

            _BaseOperation._Tracker = null;
        }

        public IList<string> GetStackTrace()
        {
            var result = new List<string>();

            var operation = this;
            do
            {
                result.Add(string.IsNullOrEmpty(operation.Name) ? operation.GetType().FullName : operation.Name);
                operation = operation.ParentOperation;
            } while (operation != null);

            return new ReadOnlyCollection<string>(result);
        }

        public IResourceConstraint ResourceConstraint
        {
            get { return _ResourceConstraint; }
            set
            {
                if (_OperationStatus != OperationStatus.NotStarted)
                {
                    throw new InvalidOperationException("The ResourceConstraint can only be set before the operation has started.");
                }
                _ResourceConstraint = value;
            }
        }

		/// <summary>
        /// Gets or sets the timeout for this asyncronous method. 
        /// </summary>
        /// <value>The timeout.</value>
        /// <remarks>This property can be modified during the execution of the operation.</remarks>
        public TimeSpan? Timeout
        {
            get { return _Timeout; }
            set
            {
				if (value == _Timeout)
				{
					return;
				}
            	_Timeout = value;

            	ResetTimeout();
            }
        }

		public void ResetTimeout()
		{
			if (_TimeoutHandle != null)
			{
				lock (_TimeoutLock)
				{
					if (_TimeoutHandle != null)
					{
						UnregisterTimeoutCallback();
						if (_Timeout != null)
						{
							RegisterTimeoutCallback();
						}
					}
				}
			}
		}

		/// <summary
        /// Gets the asynchronous operation that is currently executed on the active thread.
        /// </summary>
        /// <value>The current.</value>
        public static AsyncOperation Current { get { return _Current; } }


        /// <summary>
        /// Gets the synchronization context which is used when invoking events on this instance.
        /// </summary>
        /// <value>The synchronization context.</value>
        public SynchronizationContext SynchronizationContext { get { return _SynchronizationContext; } }

        /// <summary>
        /// Gets the name of the operation.
        /// </summary>
        /// <value>The name.</value>
        public string Name { get { return _Name; } }

        /// <summary>
        /// Gets the id of the operation.
        /// </summary>
        /// <value>The id.</value>
        public Guid Id { get { return _Id; } }

        //internal Dictionary<Type, OperationActionHandler> OperationActionHandler { get { return _OperationActionHandler; } }

        /// <summary>
        /// Gets or sets the maximum number of child operations which are executed in paralell when a
        /// <see cref="ParallelOperation"/> action is returned from the operation being executed.
        /// </summary>
        /// <value>The maximum parallel child operations.</value>
        public int MaximumParallelChildOperations { get { return _MaximumParallelChildOperations; } set { _MaximumParallelChildOperations = value; } }


        /// <summary>
        /// Gets the error that was returned from this operation.
        /// </summary>
        /// <value>The error.</value>
        public Exception Error { get { return _Error; } }


        /// <summary>
        /// Gets a <see cref="WaitHandle"/> which can be used to block a thread until this operation is completed.
        /// </summary>
        /// <value>The operation completed signal.</value>
        public WaitHandle OperationCompletedSignal
        {
            get
            {
                if (_OperationCompletedSignal == null) _OperationCompletedSignal = new ManualResetEvent(false);
                return _OperationCompletedSignal;
            }
        }

        /// <summary>
        /// Gets the operation status.
        /// </summary>
        /// <value>The operation status.</value>
        public OperationStatus OperationStatus { get { return _OperationStatus; } }

        internal static Trace Trace { get { return _Trace; } }

        internal AsyncOperation ParentOperation { get { return _Parent; } }
        public event AsyncCompletedEventHandler Completed;

        /// <summary>
        /// Occurs when the progress status of the operation changes.
        /// </summary>
        public event ProgressStatusChangedEventHandler ProgressChanged;


        /// <summary>
        /// Defines an event handler used in event based asynchronous scenarios (like <see cref="Socket.SendAsync"/>).
        /// </summary>
        /// <param name="sender">The source of the event</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        /// <remarks>Use from inside the async operation via <c>AsyncOperation.Current.EventHandler</c></remarks>
        public void EventHandler(object sender, EventArgs e)
        {
			if (Interlocked.Increment(ref _EventCallbacksOccurred) >= 0)
			{
				Advance();
			}
        }

        private AdvanceAction ExecuteWaitForAsyncResultOperation(OperationAction action)
        {
            var op = (WaitForAsyncResultOperation) action;

            if (!EventBasedAsyncResultAttribute.IsEventBasedAsyncResult(op.AsyncResult))
            {
                RegisterWaithandleCallback(op.AsyncResult.AsyncWaitHandle, TimeSpan.FromMilliseconds(-1));
            }
            return AdvanceAction.ContinueAsynchronous;
        }

        private AdvanceAction ExecuteWaitForEventOperationHandler()
        {
        	if (Interlocked.Decrement(ref _EventCallbacksOccurred) >= 0) return AdvanceAction.ContinueSynchronous;

			return AdvanceAction.ContinueAsynchronous;
        }

    	private void RegisterTimeoutCallback()
        {
            if (_Timeout != null)
            {
            	_TimeoutHandle = Dispatcher.RegisterCallback(DateTime.Now + _Timeout.Value, Timeout_Callback);
            }
        }

        private void Timeout_Callback(object state)
        {
            lock (_TimeoutLock)
            {
                _TimeoutHandle = null;
                _OperationStatus = OperationStatus.Timedout;
            }
            Advance(true);
        }

        private AdvanceAction ExecuteWaitOperationHandler(OperationAction action)
        {
            var waitAction = (WaitAction) action;

            lock (_TimeoutLock)
            {
                _WaitOperationCookie = Dispatcher.RegisterCallback(waitAction.WaitUntil, delegate { Advance(); });
            }

            return AdvanceAction.ContinueAsynchronous;
        }

        private AdvanceAction ExecuteBaseOperationHandler()
        {
            if (_BaseOperation == null)
            {
                throw new InvalidOperationException("The base operation is not set.");
            }

            _Trace.Verbose("Executing base operation {0}", _BaseOperation.Name);
            AddChildOperation(_BaseOperation, new List<Type>());

            return AdvanceAction.ContinueAsynchronous;
        }


        internal static void StartAsync(AsyncOperation operation)
        {
            ThreadPool.QueueUserWorkItem(delegate
                                             {
                                                 operation.Advance();
                                             });
        }

        private AdvanceAction ParallelOperationHandler(OperationAction obj)
        {
            var operation = (ParallelOperation) obj;

            _Trace.Verbose(0, "Executing {0} child operations in paralell ({0} at a time)", operation.Operations.Count,
                           Math.Min(MaximumParallelChildOperations, operation.MaximumParallelOperations));
            _MaximumParallelChildOperations = operation.MaximumParallelOperations;

            var children = new List<string>();

            foreach (NestedOperation nestedOperation in operation.Operations)
            {
                children.Add(nestedOperation.Operation.Name);
                AddChildOperation(nestedOperation.Operation, nestedOperation.ExceptionsToHandle);
            }
            _Trace.Data(TraceEventType.Verbose, 0, children.ToArray());

            RunChildOperations();

            return AdvanceAction.ContinueAsynchronous;
        }

        private AdvanceAction RethrowOperationHandler(OperationAction obj)
        {
            var operation = (RethrowOperation) obj;

            _Trace.Verbose(0, "Rethrowing {0}: {1}", operation.Exception.GetType().FullName, operation.Exception);

            HandleException(operation.Exception);

            return AdvanceAction.Break;
        }

        private void HandleException(Exception exception)
        {
            _Error = exception;

            if (!_Error.Data.Contains("LogicalStackTrace"))
            {
                _Error.Data["LogicalStackTrace"] = GetStackTrace();
            }

            if (_OperationStatus != OperationStatus.Timedout)
            {
                _OperationStatus = OperationStatus.Failed;
            }
        }

        private AdvanceAction NestedOperationHandler(OperationAction obj)
        {
            var operation = (NestedOperation) obj;

            AddChildOperation(operation.Operation, operation.ExceptionsToHandle);

            return AdvanceAction.ContinueAsynchronous;
        }

        private void AddChildOperation(AsyncOperation operation, List<Type> exceptionsToHandle)
        {
            operation._Parent = this;

            lock (_ChildOperationsLock)
            {
                _ChildOperations.Add(new OperationInfo(operation, exceptionsToHandle));
            }

            RunChildOperations();
        }

        private void RunChildOperations()
        {
            int index = 0;

            lock (_ChildOperationsLock)
            {
                while (index < _ChildOperations.Count &&
                       _ChildOperations[index].Operation.OperationStatus != OperationStatus.NotStarted)
                {
                    index++;
                }

                while (index < Math.Min(_ChildOperations.Count, _MaximumParallelChildOperations))
                {
                    AsyncOperation operation = _ChildOperations[index].Operation;

                    //_Trace.Transfer(0, operation.Id, "Executing child operation {0}", operation.Name);

                    operation.Run();
                    index++;
                }
            }
        }


        private AdvanceAction OperationProgressHandler(OperationAction obj)
        {
            if (_Parent != null) return _Parent.OperationProgressHandler(obj);

            var action = (OperationProgress) obj;


            OnProgressChanged(new ProgressStatusChangedEventArgs(action.Status, action.ProgressPercentage, _UserState));

            return AdvanceAction.ContinueSynchronous;
        }

        private void OnProgressChanged(ProgressStatusChangedEventArgs e)
        {
            if (_RunningSynchronously) return;

            if (ProgressChanged != null) _SynchronizationContext.Send(delegate { ProgressChanged(this, e); }, null);
        }

        private AdvanceAction WaitOperationActionHandler(OperationAction action)
        {
            var operation = (WaitForHandleOperation) action;

            RegisterWaithandleCallback(operation.WaitHandle, operation.Timeout.GetValueOrDefault(TimeSpan.FromMilliseconds(-1)));

            return AdvanceAction.ContinueAsynchronous;
        }

        private void RegisterWaithandleCallback(WaitHandle handle, TimeSpan timeout)
        {
            ThreadPool.RegisterWaitForSingleObject(handle, WaitOperationHandle_Callback, null, timeout, true);
        }

        private void WaitOperationHandle_Callback(object state, bool timedOut)
        {
//            if (timedOut) return;

            ThreadPool.QueueUserWorkItem(state1 => Advance(timedOut));
        }

		private void Advance()
        {
            Advance(false);
        }

        private void Advance(bool timedOut)
        {
            lock (_AdvanceLock)
            {
                if (!timedOut && _OperationStatus == OperationStatus.Timedout)
                {
                    return;
                }

                bool finished;
                do
                {
                    try
                    {
                        if (_OperationStatus == OperationStatus.ExecutionPending)
                        {
                            _OperationStatus = OperationStatus.Running;
                        }

                        if (timedOut)
                        {
                            if (_WaitOperationCookie != null)
                            {
                                Dispatcher.CancelCallback(_WaitOperationCookie);
                            }
                            HandleException(new TimeoutException());
                            break;
                        }


                        //Trace.ActivityId = Id;
                        Trace.Verbose("Executing next step of the operation");
                        _Current = this;
                        finished = !_OperationsEnumerator.MoveNext();
                        //Trace.ActivityId = Id;
                    }
                    catch (Exception ex)
                    {
                        //Trace.ActivityId = Id;
                        _Trace.Error(1019, "An exception of type {0} occured during the execution of the operation step: {1}",
                                     ex.GetType().FullName, ex);
                        HandleException(ex);

                        break;
                    }
                    if (!finished)
                    {
                    	var action = ProcessOperationResult(_OperationsEnumerator.Current);
                    
                    	if (action == AdvanceAction.ContinueAsynchronous) return;
                    	if (action == AdvanceAction.Break) break;

                    	if (_OperationStatus != OperationStatus.Running)
                        {
                            finished = true;
                        }
                    }
                    else _OperationStatus = OperationStatus.Completed;
                } while (!finished);

                UnwindOperation();

                if (_Error != null)
                {
                    PropagateExceptionToParent();
                }
            }
        }

    	protected virtual AdvanceAction ProcessOperationResult(OperationAction action)
		{
			if (action is WaitForHandleOperation) return WaitOperationActionHandler(action);
			if (action is WaitForAsyncCompletionOperation) return ExecuteWaitForAsyncCompletionOperation();
			if (action is WaitForAsyncResultOperation) return ExecuteWaitForAsyncResultOperation(action);
			if (action is NestedOperation) return NestedOperationHandler(action);
			if (action is OperationProgress) return OperationProgressHandler(action);
			if (action is RethrowOperation) return RethrowOperationHandler(action);
			if (action is ParallelOperation) return ParallelOperationHandler(action);
			if (action is ExecuteBaseOperation) return ExecuteBaseOperationHandler();
			if (action is WaitAction) return ExecuteWaitOperationHandler(action);
			if (action is WaitForEventOperation) return ExecuteWaitForEventOperationHandler();

    		throw new NotSupportedException(string.Format("The operation action {0} is not supported.",
    		                                              action.GetType().FullName));
		}

    	private static AdvanceAction ExecuteWaitForAsyncCompletionOperation()
    	{
    		return AdvanceAction.ContinueAsynchronous;
    	}

    	private void UnwindOperation()
        {
			// Only call this method once!
			if (Interlocked.Exchange(ref _Unwinded, 1) == 1) return;

			try
			{
				AbortChildOperations();
			}
			catch (Exception ex)
			{
				_Trace.Error(1020, "Failed to abort child operations. Error: {0}", ex);
			}
			finally
			{
				var disposable = _Operation as IDisposable;
				if (disposable != null)
				{
					_Trace.Verbose("Completing the execution of the operation");
					try
					{
						disposable.Dispose();
					}
					catch (Exception ex)
					{
						_Trace.Error(1021, "Failed to dispose operation. Error: {0}", ex);
					}
				}
				CompleteOperation();
			}
        }

        private void PropagateExceptionToParent()
        {
            if (_Parent != null)
            {
                //Trace.Transfer(0, _Parent.Id, "Propagating the current exception to the parent operation");
                _Parent.HandleExceptionFromChildOperation(Error, this);
            }
        }

        private void CompleteOperation()
        {
			if (_TimeoutHandle != null)
			{
				lock (_TimeoutLock)
				{
					if (_TimeoutHandle != null)
					{
						UnregisterTimeoutCallback();
						_TimeoutHandle = null;
					}
				}
			}

        	_ResourceReservations.ReleaseOperation(this);

            if (_Tracker != null)
            {
            	_Tracker.Unregister(Name, _UserState, this);
            }

            // Only remove this operation from the parent operation if the operation completed without exception
            // The PropagateExceptionToParent takes care of this
            if (_Parent != null && _Error == null)
            {
                //_Trace.Transfer(0, _Parent.Id, "Removing operation from the parents list of child operations");
                _Parent.RemoveChildOperation(this);
            }

            if (_OperationCompletedSignal != null)
            {
                _Trace.Verbose("Signaling waiting processes that this operation is no longer running.");
                _OperationCompletedSignal.Set();
            }

            switch (OperationStatus)
            {
                case OperationStatus.Completed:
                    OnCompleted(new AsyncCompletedEventArgs(null, false, _UserState));
                    break;
                case OperationStatus.Aborted:
                    OnCompleted(new AsyncCompletedEventArgs(null, true, _UserState));
                    break;
                case OperationStatus.Failed:
                    OnCompleted(new AsyncCompletedEventArgs(_Error, false, _UserState));
                    break;
                case OperationStatus.Timedout:
                    OnCompleted(new AsyncCompletedEventArgs(_Error, false, _UserState));
                    break;
            }
        }

        private void UnregisterTimeoutCallback()
        {
            DispatcherCookie dispatcherCookie = Interlocked.Exchange(ref _TimeoutHandle, null);

            if (dispatcherCookie == null)
            {
                return;
            }
            Dispatcher.TryCancelCallback(dispatcherCookie);
        }

        /// <summary>
        /// Synchronously starts the execution of this operation.
        /// </summary>
        public void RunSynchronously()
        {
            _RunningSynchronously = true;

			var operationCompletedSignal = OperationCompletedSignal;
			
			Run();

            if (_OperationStatus == OperationStatus.Running || _OperationStatus == OperationStatus.ExecutionPending)
            {
            	operationCompletedSignal.WaitOne();
            }

        	if (Error == null)
            {
                return;
            }
            if (ExceptionHelper.PreserveExceptionStackTrace(Error))
            {
                throw Error;
            }

            throw new AsyncOperationFailedException(
                string.Format("The operation has failed. See the inner exception for more details. Error: {0}", Error.Message), Error);
        }


        private void OnCompleted(AsyncCompletedEventArgs e)
        {
            if (_RunningSynchronously) return;

            if (Completed != null) _SynchronizationContext.Send(delegate { Completed(this, e); }, null);
        }

        /// <summary>
        /// Asynchronously starts the execution of this operation.
        /// </summary>
        public void Run()
        {
            if (_Tracker != null)
            {
                //Trace.ActivityId = Id;
                _Tracker.Register(_Name, _UserState, this, _AllowMultipleInvocations);
            }

            _OperationStatus = OperationStatus.ExecutionPending;

            _StartTime = DateTime.Now;
            RegisterTimeoutCallback();

            if (_RunningSynchronously)
            {
                _ResourceReservations.RegisterOperation(this);
            }
            else
            {
                ThreadPool.QueueUserWorkItem(delegate { _ResourceReservations.RegisterOperation(this); });
            }
        }

       

        private void RemoveChildOperation(AsyncOperation childOperation)
        {
            bool advance;

            if (childOperation == null) throw new ArgumentNullException("childOperation");

            lock (_ChildOperationsLock)
            {
                _ChildOperations.RemoveAll(info => info.Operation == childOperation);

                advance = _ChildOperations.Count == 0;
                if (!advance) RunChildOperations();
            }

            if (advance && OperationStatus == OperationStatus.Running) Advance();
        }

        private void HandleExceptionFromChildOperation(Exception ex, AsyncOperation childOperation)
        {
            OperationInfo info;

            lock (_ChildOperationsLock)
            {
                info = _ChildOperations.Find(oi => oi.Operation == childOperation);

                if (info == null || !info.HandlesException(ex))
                {
                    // Fail the current operation
                    HandleException(ex);

                    RemoveChildOperation(childOperation);

                    UnwindOperation();

                    // Bubble the exception to the parent operation
                    if (_Parent != null) _Parent.HandleExceptionFromChildOperation(ex, this);
                }
                else
                {
                    RemoveChildOperation(childOperation);
                }
            }
        }

        private void AbortChildOperations()
        {
            AsyncOperation child;

            if (_ChildOperations.Count == 0) return;

            lock (_ChildOperationsLock)
            {
                for (int i = _ChildOperations.Count - 1; i >= 0; i--)
                {
                    child = _ChildOperations[i].Operation;
                    _ChildOperations.RemoveAt(i);

                    child.Cancel();
                }
            }
        }

        /// <summary>
        /// Gracefully aborts the execution of this method.
        /// </summary>
        public void Cancel()
        {
            if (_OperationStatus == OperationStatus.Running) _OperationStatus = OperationStatus.Aborted;

            UnwindOperation();
        }
    }
}